package com.wmx.poi.word;

import cn.hutool.core.util.ObjectUtil;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.xmlbeans.XmlCursor;
import org.junit.Test;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth;

import java.beans.PropertyDescriptor;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 根据 .docx 格式的 Word 文件模板动态替换占位符生成最终文件
 * word 模板占位符写法规范强制为：${param}
 *
 * @author wangMaoXiong
 * @version 1.0
 * @date 2021/11/9 10:41
 */
public class TemplateDoc2Test {

    /**
     * IGNORE_TG：标识忽略
     * START_TG：标识已读取到'$'字符
     * READ_TG：标识已读取到'{'字符
     * TABLE_KEY：在文档中需要替换插入表格的位置
     */
    private static final int IGNORE_TG = 0;
    private static final int START_TG = 1;
    private static final int READ_TG = 2;
    private static final String TABLE_KEY = "$key";

    /**
     * 解析 docx 文件模板填充数据，并生成到目标文件
     *
     * @param inputStream ：docx 模板文件字节输入流
     * @param dataMap     ：待填充的数据
     * @param destFile    ：目标文件存储位置
     */
    public static <T> void docxTemplateParser(InputStream inputStream, Map<String, Object> dataMap, List<String> tableHead, List<T> tableBody, File destFile) throws Exception {
        FileOutputStream fileOutputStream = null;
        BufferedOutputStream bufferedOutputStream = null;
        XWPFDocument xwpfDocument = null;
        try {
            xwpfDocument = new XWPFDocument(inputStream);
            List<XWPFParagraph> paragraphList = xwpfDocument.getParagraphs();

            //获取占位符，并且将占位符需要替换的值写入
            List<int[]> placeholderList = getPlaceholderList(paragraphList, dataMap);
            //清除占位符信息
            clearPlaceholder(paragraphList, placeholderList);
            //插入表格
            createTable(xwpfDocument, TABLE_KEY, tableHead, tableBody);

            fileOutputStream = new FileOutputStream(destFile);
            bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
            xwpfDocument.write(bufferedOutputStream);
        } catch (IOException e) {
            throw e;
        } finally {
            if (bufferedOutputStream != null) {
                bufferedOutputStream.flush();
                bufferedOutputStream.close();
            }
            if (fileOutputStream != null) {
                fileOutputStream.flush();
                fileOutputStream.close();
            }
            if (xwpfDocument != null) {
                xwpfDocument.close();
            }
        }
    }

    /**
     * 清除段落中的占位符信息
     *
     * @param paragraphList   ：文档段落
     * @param placeholderIndexList ： 占位符位置信息
     */
    private static void clearPlaceholder(List<XWPFParagraph> paragraphList, List<int[]> placeholderIndexList) {
        if (ObjectUtils.isEmpty(paragraphList) || ObjectUtils.isEmpty(placeholderIndexList)) {
            return;
        }
        int[] currentPlaceholder = placeholderIndexList.get(0);
        //完整的占位符信息，如：${fiscal_year}、${name}、${num37}
        StringBuilder placeholderSb = new StringBuilder();
        //清除占位符之后的最终文本内容
        StringBuilder finalTestSb = new StringBuilder();
        for (int i = 0; i < paragraphList.size(); i++) {
            XWPFParagraph xwpfParagraph = paragraphList.get(i);
            List<XWPFRun> xwpfRuns = xwpfParagraph.getRuns();
            for (int j = 0; j < xwpfRuns.size(); j++) {
                XWPFRun xwpfRun = xwpfRuns.get(j);
                String xwpfRunText = xwpfRun.getText(xwpfRun.getTextPosition());
                if (StringUtils.isBlank(xwpfRunText)) {
                    continue;
                }
                char[] xwpfRunTextChars = xwpfRunText.toCharArray();
                for (int m = 0; m < xwpfRunTextChars.length; m++) {
                    char xwpfRunTextChar = xwpfRunTextChars[m];
                    if (null == currentPlaceholder) {
                        finalTestSb.append(xwpfRunTextChar);
                        continue;
                    }
                    // 排除'$'和'}'两个字符之间的字符
                    int start = currentPlaceholder[0] * 1000000 + currentPlaceholder[1] * 500 + currentPlaceholder[2];
                    int end = currentPlaceholder[3] * 1000000 + currentPlaceholder[4] * 500 + currentPlaceholder[5];
                    int cur = i * 1000000 + j * 500 + m;
                    if (start <= cur && cur <= end) {
                        placeholderSb.append(xwpfRunTextChar);
                    } else {
                        finalTestSb.append(xwpfRunTextChar);
                    }
                    //判断是否是占位符结尾，如果是则下一轮开始获取新的占位符
                    if (placeholderSb.toString().endsWith("}")) {
                        placeholderIndexList.remove(0);
                        if (ObjectUtils.isEmpty(placeholderIndexList)) {
                            currentPlaceholder = null;
                            continue;
                        }
                        currentPlaceholder = placeholderIndexList.get(0);
                        placeholderSb.delete(0, placeholderSb.length());
                    }
                }
                xwpfRun.setText(finalTestSb.toString(), xwpfRun.getTextPosition());
                finalTestSb.delete(0, finalTestSb.length());
            }
        }
    }

    /**
     * 获取段落中的占位符信息，并且在占位符后面填充值
     *
     * @param xwpfParagraphs ：文档中的段落(不含表格)
     * @param dataMap       : 要替换的占位符数据
     * @return
     */
    private static List<int[]> getPlaceholderList(List<XWPFParagraph> xwpfParagraphs, Map<String, Object> dataMap) {
        // 存储占位符名称信息
        List<String> placeholderNameList = new ArrayList<>();
        // 存储段落中所有的占位符位置信息
        List<int[]> placeholderIndexList = new ArrayList<>();
        // 存储当前循环的占位符位置信息
        // 0：'$'字符在XWPFParagraph集合中下标
        // 1：'$'字符在XWPFRun集合中下标
        // 2：'$'字符在text.toCharArray()数组下标
        // 3: '}'字符在XWPFParagraph集合中下标
        // 4: '}'字符在XWPFRun集合中下标
        // 5：'}'字符在text.toCharArray()数组下标
        int[] currentPlaceholder = new int[6];

        // 当前标识
        int modeTg = IGNORE_TG;
        //组装完整的占位符信息，如：${fiscal_year}、${name}、${num37}
        StringBuilder placeholderSb = new StringBuilder();
        //closureSb 值比如：}、3}、um3}、${num3} 等等带 '}' 的占位符信息，不会有占位符以外的正文.
        StringBuilder closureSb = new StringBuilder();
        for (int i = 0; i < xwpfParagraphs.size(); i++) {
            XWPFParagraph xwpfParagraph = xwpfParagraphs.get(i);
            //XWPFRun 对象使用一组公共属性定义文本区域，相当于是段落中各种样式的文本信息.
            List<XWPFRun> xwpfRuns = xwpfParagraph.getRuns();
            for (int j = 0; j < xwpfRuns.size(); j++) {
                XWPFRun xwpfRun = xwpfRuns.get(j);
                //int getTextPosition()：此元素指定相对于周围未定位文本的默认基线，此运行中文本的升高或降低量。这允许在不改变内容字体大小的情况下重新定位文本。
                //String getText(int pos):返回此文本运行的字符串内容
                //xwpfRunText 相当于是段落中的纯文本
                String xwpfRunText = xwpfRun.getText(xwpfRun.getTextPosition());
                if (StringUtils.isBlank(xwpfRunText)) {
                    continue;
                }
                char[] xwpfRunTextChars = xwpfRunText.toCharArray();
                //newVal 值比如：${、fiscal_year、}2022、年、${name}蚩尤后裔，包含了占位符以及需要替换进去的数据跟在 '}'符号后面
                String newVal = "";
                for (int m = 0; m < xwpfRunTextChars.length; m++) {
                    //xwpfRunTextChar 包含了段落中的每一个字符
                    char xwpfRunTextChar = xwpfRunTextChars[m];
                    closureSb.append(xwpfRunTextChar);
                    switch (xwpfRunTextChar) {
                        case '$': {
                            modeTg = START_TG;
                            placeholderSb.append(xwpfRunTextChar);
                        }
                        break;
                        case '{': {
                            if (modeTg == START_TG) {
                                placeholderSb.append(xwpfRunTextChar);
                                modeTg = READ_TG;
                                currentPlaceholder[0] = i;
                                currentPlaceholder[1] = j;
                                currentPlaceholder[2] = m - 1;
                            } else if (modeTg == READ_TG) {
                                placeholderSb.delete(0, placeholderSb.length());
                                modeTg = IGNORE_TG;
                            }
                        }
                        break;
                        case '}': {
                            if (modeTg == READ_TG) {
                                modeTg = IGNORE_TG;
                                placeholderSb.append(xwpfRunTextChar);
                                //得到完整的占位符名称，如：${fiscal_year}、${name}、${num37}
                                String placeholderName = placeholderSb.toString();
                                //根据占位符获取需要替换的数据
                                Object placeholderValue = dataMap.get(placeholderName);
                                placeholderNameList.add(placeholderName);
                                if (placeholderValue != null && StringUtils.isNotBlank(placeholderValue.toString())) {
                                    newVal += closureSb.toString() + placeholderValue;
                                    placeholderIndexList.add(currentPlaceholder);
                                    closureSb.delete(0, closureSb.length());
                                }
                                currentPlaceholder[3] = i;
                                currentPlaceholder[4] = j;
                                currentPlaceholder[5] = m;
                                currentPlaceholder = new int[6];
                                placeholderSb.delete(0, placeholderSb.length());
                            } else if (modeTg == START_TG) {
                                modeTg = IGNORE_TG;
                                placeholderSb.delete(0, placeholderSb.length());
                            }
                        }
                        default: {
                            if (modeTg == READ_TG) {
                                placeholderSb.append(xwpfRunTextChar);
                            } else if (modeTg == START_TG) {
                                modeTg = IGNORE_TG;
                                placeholderSb.delete(0, placeholderSb.length());
                            }
                        }
                    }
                }
                newVal += closureSb.toString();
                closureSb.delete(0, closureSb.length());
                xwpfRun.setTextPosition(0);
                xwpfRun.setText(newVal, xwpfRun.getTextPosition());
            }
        }
        System.out.println("模板文件中占位符信息：");
        placeholderNameList.stream().forEach(item -> System.out.println("\t" + item));
        return placeholderIndexList;
    }

    /**
     * 创建一个表格插入到 positionKey 标记的位置
     *
     * @param xwpfDocument ：docx 文档
     * @param positionKey  ：需要插入的位置，比如 ${tableKey1}
     * @param tableHead    ：表头，支持单表头
     * @param tableBody    ：表格内容
     * @throws Exception
     */
    public static <T> void createTable(XWPFDocument xwpfDocument, String positionKey, List<String> tableHead, List<T> tableBody) throws Exception {
        List<XWPFParagraph> paragraphList = xwpfDocument.getParagraphs();
        if (ObjectUtil.isEmpty(paragraphList) || ObjectUtil.isEmpty(tableHead) || ObjectUtil.isEmpty(tableBody)) {
            return;
        }
        //循环段落
        for (XWPFParagraph xwpfParagraph : paragraphList) {
            List<XWPFRun> xwpfRuns = xwpfParagraph.getRuns();
            for (int i = 0; i < xwpfRuns.size(); i++) {
                String text = xwpfRuns.get(i).getText(0);
                if (org.springframework.util.StringUtils.isEmpty(text)) {
                    continue;
                }
                text = text.trim();
                if (text.indexOf(positionKey) < 0) {
                    continue;
                }
                xwpfRuns.get(i).setText(text.replace(positionKey, ""), 0);
                //XmlCursor newCursor()：返回一个新的XML游标
                //游标提供对XML数据中所有标记的随机访问，以及为数据提取强类型XmlObjects的能力。如果数据不是只读的，则XML光标还允许修改数据。
                //第一次使用游标通常会强制XML文档进入内存
                XmlCursor xmlCursor = xwpfParagraph.getCTP().newCursor();
                //在指定游标位置插入表格
                XWPFTable xwpfTable = xwpfDocument.insertNewTbl(xmlCursor);
                CTTblPr tablePr = xwpfTable.getCTTbl().getTblPr();
                CTTblWidth width = tablePr.addNewTblW();
                width.setW(BigInteger.valueOf(8500));
                //表格插入数据
                insertInfo(xwpfTable, tableHead, tableBody);
                break;
            }
        }
    }

    /**
     * 把表格数据插入表格
     *
     * @param xwpfTable ：poi 表格对象
     * @param tableHead ：表头
     * @param tableBody ：表格数据
     * @param <T>       ：实体中属性的顺序必须和表头(tableHead)一一对应
     * @throws Exception
     */
    private static <T> void insertInfo(XWPFTable xwpfTable, List<String> tableHead, List<T> tableBody) throws Exception {
        //获取第一行
        XWPFTableRow row = xwpfTable.getRow(0);
        //获取这个东西用来改表格宽度
        CTTblPr tblPr = xwpfTable.getCTTbl().getTblPr();
        //改变长度策略为自己调整 默认为auto
        tblPr.getTblW().setType(STTblWidth.DXA);
        //设置表格宽度为7000
        tblPr.getTblW().setW(BigInteger.valueOf(8500));
        //根据头创建表格head.默认会创建一列，即从第2列开始
        for (int col = 1; col < tableHead.size(); col++) {
            // 第一行创建了多少列，后续增加的行自动增加列
            CTTcPr cPr = row.createCell().getCTTc().addNewTcPr();
            //设置单元格高度为400
            row.getCtRow().addNewTrPr().addNewTrHeight().setVal(BigInteger.valueOf(400));
            //可以用来设置单元格长度
            //CTTblWidth width = cPr.addNewTcW();
            //width.setW(BigInteger.valueOf(2000));
        }
        //循环给表格添加头信息
        for (int i = 0; i < tableHead.size(); i++) {
            //往表格里面写入头信息
            row.getCell(i).setText(tableHead.get(i));
        }
        //计数器
        int i = 0;
        //循环填充body列表（列表为vo数组）
        for (T item : tableBody) {
            //获取类
            Class<?> clazz = item.getClass();
            //获取item字段
            Field[] fields = item.getClass().getDeclaredFields();
            //创建行
            row = xwpfTable.createRow();
            //修改行高为500
            row.getCtRow().addNewTrPr().addNewTrHeight().setVal(BigInteger.valueOf(400));
            //循环获取vo类的属性
            for (Field field : fields) {
                //获取当前field的属性描述器
                PropertyDescriptor descriptor = new PropertyDescriptor(field.getName(), clazz);
                //获取field字段的get方法
                Method readMethod = descriptor.getReadMethod();
                //执行get方法并填充到表格中
                row.getCell(i).setText(String.valueOf(readMethod.invoke(item)));
                //计数器+1
                i++;
            }
            //执行完一行计数器归零
            i = 0;
        }
    }


    @Test
    public void docxTemplateParserTest() throws Exception {
        String destPath = "E:\\temp\\" + System.currentTimeMillis() + ".docx";
        String srcPath = "word/预算公开配置功能模板2.docx";
        Map<String, Object> dataMaps = getDataMaps();
        InputStream inputStream = TemplateDoc2Test.class.getClassLoader().getResourceAsStream(srcPath);

        //表头的顺序必须和正文视图对象属性的顺序保持一致
        List<String> tablHead = new ArrayList<>();
        tablHead.add("姓名");
        tablHead.add("性别");
        tablHead.add("年龄");

        List<Person> tableBody = new ArrayList<Person>(20);
        for (int i = 0; i < 20; i++) {
            Person dto = new Person("张三" + i, 22, "男");
            tableBody.add(dto);
        }
        docxTemplateParser(inputStream, dataMaps, tablHead, tableBody, new File(destPath));
        System.out.println("生成文件：" + destPath);
    }

    /**
     * 获取需要插入到文档中的数据
     *
     * @return
     */
    private Map<String, Object> getDataMaps() {
        Map<String, Object> dataMap = new HashMap<>(64);

        dataMap.put("${fiscal_year}", "2022");
        dataMap.put("${year}", "2022");
        dataMap.put("${name}", "蚩尤后裔");
        dataMap.put("${agency_name}", " 邵阳市北塔区财政局");
        int start = 1;
        int end = 11;
        for (int i = start; i <= end; i++) {
            dataMap.put("${num" + i + "}", String.valueOf(new SecureRandom().nextInt(100) + 3));
        }
        start = 12;
        end = 55;
        for (int i = start; i <= end; i++) {
            dataMap.put("${num" + i + "}", String.valueOf(new SecureRandom().nextFloat() * 10000));
        }
        start = 1;
        end = 10;
        for (int i = start; i <= end; i++) {
            dataMap.put("${no" + i + "}", String.valueOf(i));
        }
        int row = 3;
        int column = 5;
        for (int i = 1; i <= row; i++) {
            for (int j = 1; j < column; j++) {
                dataMap.put("${col" + i + j + "}", String.valueOf(new SecureRandom().nextFloat() * 10000));
            }
        }
        dataMap.put("${str1}", "防洪救灾");
        dataMap.put("${str2}", "防洪办");
        dataMap.put("${str3}", "预算");
        dataMap.put("${oldYear}", "2021");
        return dataMap;
    }

}
